// Copyright (c) 2014 Narin Persad

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
 

//-----------------------------------------------------------------------


using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace MvcSimpleQuery.QueryTree
{
    public static class Clause
    {
        /// <summary>
        /// Creates an Expression to be specified in CreateQuery 
        /// </summary>
        /// <typeparam name="T">The class of IQueryable objects</typeparam>
        /// <param name="queries">array of conditions</param>
        /// <param name="queryableData">queryable data needed to extract Expression properties</param>
        /// <returns></returns>
        public static Expression CreateExpression<T>(this List<Conditions> queries, IQueryable<T> queryableData) where T : class
        {

            var pexp = Expression.Parameter(typeof(T), "x");

            return (Expression)Expression.Call(typeof(Queryable), "Where",
                     new Type[] { queryableData.ElementType },
                     queryableData.Expression,
                     queries != null ?
                     Expression.Lambda<Func<T, bool>>(queries.buildFilter<T>(pexp), new ParameterExpression[] { pexp })
                     : Expression.Lambda<Func<T, bool>>(MemberExpression.NotEqual(pexp, Expression.Constant(null)), new ParameterExpression[] { pexp }));


        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T">The type in the collection that will be queried</typeparam>
        /// <param name="queryTree">a List of conditions</param>
        /// <param name="pexp">The Expression parameter of the class type 
        /// </param>
        /// <returns>a LINQ expression</returns>
        /// <exception cref=" MvcSimpleQuery.QueryTree.ClauseNullException">thrown when one of the conditions in the query is null</exception>
       /// <exception cref="ArgumentException">thrown when "Field" is passed instead of a class property</exception>
        private static Expression buildFilter<T>(this List<Conditions> queryTree, ParameterExpression pexp)
        {
            Expression expression;
            try
            {
                expression = queryTree.buildQueryRecursive<T>(pexp);
            }
            catch (ArgumentNullException argumentNullException)
            {
                throw new ClauseNullException("One or more arguments in the incoming query was null", argumentNullException);
            }
            catch (ArgumentException )
            {
                throw;
            }
            return expression;
        }

        /// <summary>
        /// Recursively build the entire LINQ expression
        /// </summary>
        /// <typeparam name="T">The type in the collection that will be queried</typeparam>
        /// <param name="queryTree">the List of conditions </param>
        /// <param name="audit"></param>
        /// <returns>a LINQ expression</returns>
        private static Expression buildQueryRecursive<T>(this List<Conditions> queryTree, ParameterExpression audit)
        {
            Conditions current = null;
            IEnumerator<Conditions> enumerator = queryTree.GetEnumerator();
            enumerator.MoveNext();
            Expression expression = enumerator.Current.condition.expression<T>(audit);
            do
            {
                if (current != null)
                {
                    if (current.condition.Conjunction != null)
                    {
                        string lower = current.condition.Conjunction.ToLower();
                        if (lower != null)
                        {
                            if (lower == "and")
                            {
                                expression = Expression.AndAlso(expression, (enumerator.Current.condition.isSubCondition ? enumerator.Current.conditions.buildQueryRecursive<T>(audit) : enumerator.Current.condition.expression<T>(audit)));
                            }
                            else if (lower == "or")
                            {
                                expression = Expression.OrElse(expression, (enumerator.Current.condition.isSubCondition ? enumerator.Current.conditions.buildQueryRecursive<T>(audit) : enumerator.Current.condition.expression<T>(audit)));
                            }
                        }
                    }
                }
                current = enumerator.Current;
            }
            while (enumerator.MoveNext());
            return expression;
        }
        /// <summary>
        /// creates an Expression that can be used in LINQ
        /// </summary>
        /// <typeparam name="T">The type in the collection that will be queried</typeparam>
        /// <param name="condition">The condition that will be parsed to create ONE expression</param>
        /// <param name="pexp">the parameter that will be used in the expresion</param>
        /// <returns>The expression that is created from the Condition</returns>
        private static Expression expression<T>(this Condition condition, ParameterExpression pexp)
        {

            Type type;
            Expression parsedVar;
            MemberExpression left_Member_Expression = Expression.Property(pexp, condition.Field);
            PropertyInfo property = typeof(T).GetProperty(condition.Field);
            MethodInfo method = null;
            Expression rightExpression = null;
            if (condition.Value != null)
            {
                type = (!property.PropertyType.IsGenericType ? property.PropertyType : property.PropertyType.GetGenericArguments()[0]);
                Type[] typeArray = new Type[] { typeof(string) };
                method = type.GetMethod("Parse", typeArray);

                parsedVar = property.PropertyType != typeof(string) ? Expression.Constant(Expression.Lambda<Func<object>>(Expression.Convert(Expression.Call(method, Expression.Constant(condition.Value)), typeof(object)), new ParameterExpression[0]).Compile()())
                :
                parsedVar = Expression.Constant(condition.Value);

                rightExpression = property.PropertyType.IsGenericType ? Expression.Convert(parsedVar, property.PropertyType) : parsedVar;

            }
            else
            {
                rightExpression = Expression.Constant(null);
            }
            return createExpression(condition, left_Member_Expression, rightExpression);
        }
        /// <summary>
        /// joins to expressesions by an operator
        /// </summary>
        /// <param name="condition">the condition</param>
        /// <param name="left_Member_Expression">The left expression on the left side of the operator</param>
        /// <param name="rightExpression">the expression on the right side of the operator</param>
        /// <returns>an expression</returns>
        private static Expression createExpression(Condition condition, MemberExpression left_Member_Expression, Expression rightExpression)
        {
            string operation = condition.Operation;
            if (operation != null)
            {
                switch (operation)
                {
                    case "lt":
                        {
                            return Expression.LessThan(left_Member_Expression, rightExpression);
                        }
                    case "lte":
                        {
                            return Expression.LessThanOrEqual(left_Member_Expression, rightExpression);
                        }
                    case "gt":
                        {
                            return Expression.GreaterThan(left_Member_Expression, rightExpression);
                        }
                    case "gte":
                        {
                            return Expression.GreaterThanOrEqual(left_Member_Expression, rightExpression);
                        }
                    case "eq":
                        {
                            return Expression.Equal(left_Member_Expression, rightExpression);
                        }
                    case "neq":
                        {
                            return Expression.NotEqual(left_Member_Expression, rightExpression);
                        }
                    default:
                        {
                            throw new Exception();
                        }
                }
            }
            throw new Exception();
        }
    }
}
